import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Iterator;

public class Neuron extends DrawObject
{
	// DrawObject abilities:
	@Override
	public boolean IsNeuron()
	{
		return true;
	}

	@Override
	public boolean CanBeDeletedByUser()
	{
		return true;
	}

	@Override
	public boolean CanBeExcitedByUser()
	{
		return true;
	}

	@Override
	public boolean CanBeMovedByUser()
	{
		return true;
	}

	@Override
	public boolean CanBeSelectedByUser()
	{
		return true;
	}
	// end of DrawObject abilities

	private ArrayList<NeuronInputField> InputFields = new ArrayList<NeuronInputField>(); // always add field before node!
	private ArrayList<NeuronInputNode> InputNodes = new ArrayList<NeuronInputNode>();
	private ArrayList<NeuronOutputNode> OutputNodes = new ArrayList<NeuronOutputNode>();

	private int TickCountSinceLastActivation = 0;

	protected class NeuronInputDrawObjects
	{
		private NeuronInputField Field;
		private NeuronInputNode Node;

		protected NeuronInputDrawObjects(NeuronInputField Field, NeuronInputNode Node)
		{
			this.Field = Field;
			this.Node = Node;
		}

		public NeuronInputField GetField()
		{
			return Field;
		}

		public NeuronInputNode GetNode()
		{
			return Node;
		}
	};

	public Neuron(Point Pos)
	{
		SetPos(Pos);
		SetSize(new Point(100, 100));
		InitializeExcitement(ExcitementOnceMilliseconds);
		SetCompoundId(GetNewCompoundId());
	};

	public static String ExtractHormoneName(String Line)
	{
		// example: "Adrenaline=50", or "Aggression=-30", or "Hormone.Sex=+30", just "x=y[percent]"

		try
		{
			String HormoneName = "";
			int Pos = Line.indexOf("=");
			if (Pos >= 0)
			{
				HormoneName = Line.substring(0, Pos);
				return HormoneName;
			}
			else
			{
				return "";
			}
		}
		catch (Exception e) // for compatibility with ExtractHormoneStrength()
		{
			return "";
		}
	};

	public static double ExtractHormoneStrength(String Line)
	{
		// example: "Adrenaline=50", or "Aggression=-30", or "Hormone.Sex=+30", just "x=y[percent]"

		try
		{
			double HormoneStrength = 0.0;
			int Pos = Line.indexOf("=");
			if (Pos >= 0)
			{
				HormoneStrength = ((double) Integer.parseInt(Line.substring(Pos + 1))) / 100.0;
				return HormoneStrength;
			}
			else
			{
				return 0.0; // default (means Hormone inactive)
			}
		}
		catch (Exception e) // we expect NumberFormatException, if any
		{
			return 0.0; // default (means Hormone inactive)
		}
	};

	public static double LimitHormoneStrength(double Strength)
	{
		if (Strength < -1.0)
			Strength = -1.0;
		if (Strength > +1.0)
			Strength = +1.0;

		return Strength;
	}

	@Override
	protected void AddExcitementForNextTick(double ExcitementAdd)
	{
		// Neuron: NeuronInputFields DIRECTLY call this.ExciteAsPartner() instead!
	}

	public void AddHormone(String HormoneName)
	{
		if (HormoneName != null && HormoneName.length() >= 1) // user can delete any Hormone from any Neuron by just setting the Neuron's Hormone name to "", after saving to and re-loading from file the Neuron does not contain the Hormone any more
		{
			if (HormoneNames == EmptyHormoneNames)
				HormoneNames = new ArrayList<String>();
			if (HormoneStrengths == EmptyHormoneStrengths)
				HormoneStrengths = new ArrayList<Double>();

			HormoneNames.add(HormoneName);
			HormoneStrengths.add(0.0);

			AutoSize(); // make space for HormoneName=HormoneStrength string printed at bottom
		}
		else
		{
			AutoSize(); // do in any case to resize after having reloaded Neuron from file
		}
	}

	public NeuronInputDrawObjects AddInputFieldAndNode()
	{ // spawns child objects

		Point NeuronInputFieldPos = new Point(Pos.x, Pos.y + InputFields.size() * 20);
		Point NeuronInputNodePos = new Point(Pos.x - 10, Pos.y + InputFields.size() * 20);

		NeuronInputField NeuronInputFieldNew = new NeuronInputField(NeuronInputFieldPos);
		NeuronInputNode NeuronInputNodeNew = new NeuronInputNode(NeuronInputNodePos);

		NeuronInputNodeNew.SetCompoundId(((Neuron) this).CompoundId);
		NeuronInputNodeNew.SetParentDrawObject(this);
		NeuronInputNodeNew.ExcitementPartner = NeuronInputFieldNew;
		NeuronInputFieldNew.SetCompoundId(((Neuron) this).CompoundId);
		NeuronInputFieldNew.SetParentDrawObject(this);
		NeuronInputFieldNew.ExcitementPartner = (Neuron) this;

		InputFields.add(NeuronInputFieldNew);
		InputNodes.add(NeuronInputNodeNew);

		AutoSize();

		return new NeuronInputDrawObjects(NeuronInputFieldNew, NeuronInputNodeNew);
	}

	public void AddInputFieldAndNode(NeuronInputField AddField, NeuronInputNode AddNode)
	{ // add child objects directly (during file reload)

		AddNode.SetParentDrawObject(this);
		AddField.SetParentDrawObject(this);

		AddNode.ExcitementPartner = AddField;
		AddField.ExcitementPartner = ((Neuron) this);

		InputFields.add(AddField);
		InputNodes.add(AddNode);

		AutoSize();
	}

	public NeuronOutputNode AddOutputNode()
	{
		NeuronOutputNode NeuronOutputNodeNew = new NeuronOutputNode(new Point(Pos.x + 100, Pos.y + Size.y / 2));

		NeuronOutputNodeNew.SetCompoundId(((Neuron) this).CompoundId);
		NeuronOutputNodeNew.SetParentDrawObject(this);

		((Neuron) this).ExcitementPartner = NeuronOutputNodeNew;

		OutputNodes.add(NeuronOutputNodeNew);

		AutoSize();

		return NeuronOutputNodeNew;
	}

	public void AddOutputNode(NeuronOutputNode AddNode)
	{
		AddNode.SetParentDrawObject(this);

		((Neuron) this).ExcitementPartner = AddNode;

		OutputNodes.add(AddNode);

		AutoSize();
	}

	@Override
	protected void AfterCollectingAllExcitementForNextTick()
	{
		if (IsInLearningNeuronState)
		{
			boolean NegativeInputFieldActivated = false;

			// If this is a LearningNeuron, the Strength of those NeuronInputFields
			// is increased by one which were excited in case that ADDITIONALLY one
			// negative NeuronInputField (the user can toggle with "n" key press)
			// was excited, too. If no negative NeuronInputField was excited, the
			// LearningNeuron behaves just like a normal Neuron, it just activates
			// its NeuronOutputNode if the internal excitement exceeds the trigger
			// value.

			for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
			{
				NeuronInputField o = i.next();
				if (o.IsNegative && o.ExcitementPartnerServedInThisTick)
				{
					NegativeInputFieldActivated = true;
					break;
				}
			}
			if (NegativeInputFieldActivated) // mandatory condition to increase any NeuronInputField.Strength value
			{
				int ServerServedCount = 0;
				@SuppressWarnings("unused")
				int ServerMissedCount = 0;

				for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
				{
					NeuronInputField o = i.next();
					if (!(o.IsNegative))
					{
						if (o.ExcitementPartnerServedInThisTick)
							ServerServedCount++;
						else
							ServerMissedCount++;
					}
				}
				if (ServerServedCount >= 1) // && ServerMissedCount == 0) // ALL required inputs excited?
				{ // currently: just increase Strength of any non-negative field which donated excitement

					for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
					{
						NeuronInputField o = i.next();
						if (!(o.IsNegative) && o.ExcitementPartnerServedInThisTick)
						{
							// o.IncreaseStrength();
							o.SetStrength(o.GetStrengthMax());
						}
					}
				}
			}
		}
	}

	private void AutoSize()
	{
		if ((InputFields.size() + HormoneNames.size()) > 100 / 20)
		{
			Size.y = InputFields.size() * 20 + HormoneNames.size() * 20;
		}
		else
		{
			Size.y = 100;
		}
	}

	public void DeleteHormone(String HormoneName)
	{
		for (int i = 0; i < HormoneNames.size(); i++)
		{
			if (HormoneNames.get(i).equals(HormoneName))
			{
				HormoneNames.remove(i);
				HormoneStrengths.remove(i);
				i--;
			}
		}

		AutoSize();
	}

	@Override
	public void Draw(Graphics g)
	{
		if (TickCountSinceLastActivation < Integer.MAX_VALUE) // for drawing only (keep longer in "activated" color as a visual help for user)
			TickCountSinceLastActivation++;

		if (!(IsWithinWindowBounds(this, 0)))
			return;

		// set font once for speed reasons
		g.setFont(GetDefaultFont());

		// background (overdraw grid)
		if (IsActivated())
			TickCountSinceLastActivation = 0;

		if (IsHighlighted || IsPartOfMultiSelection)
			SetColor(g, Color.GREEN);
		else if (TickCountSinceLastActivation < Simulation.TicksPerIOTextAreaLine())
			SetColor(g, GetExcitementColor(Color.LIGHT_GRAY));
		else
			SetColor(g, Color.LIGHT_GRAY);
		FillRect(g, Pos.x, Pos.y, Size.x, Size.y);

		// child objects
		int InputFieldWidth = -1;
		for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
		{
			NeuronInputField o = i.next();
			o.Draw(g);
			InputFieldWidth = o.Size.x;
		}
		for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
		{
			NeuronInputNode o = i.next();
			o.Draw(g);
		}
		// Hormone info drawn at end

		// excitement bar
		double ExcitementMax = GetExcitementMaxFromExcitementHistory();

		int ExcitementBarWidth = 10;
		int ExcitementBarHeight = (int) (ExcitementMax * Size.y); // WITHIN frame (don't add Pos.y)

		// actual bar
		if (DoesActivate(ExcitementMax))
			SetColor(g, Color.GREEN);
		else
			SetColor(g, Color.DARK_GRAY);
		FillRect(g, Pos.x + Size.x - ExcitementBarWidth, Pos.y + Size.y - ExcitementBarHeight, ExcitementBarWidth, ExcitementBarHeight);

		// bar background (do not highlight, else bar not visible)
		SetColor(g, Color.LIGHT_GRAY);
		FillRect(g, Pos.x + Size.x - ExcitementBarWidth, Pos.y, ExcitementBarWidth, Size.y - ExcitementBarHeight);

		// excitement bar trigger level marking
		int ExcitementBarTriggerLevelMarkingY = Size.y - (int) (ExcitementTriggerValue * Size.y);
		SetColor(g, Color.RED);
		FillRect(g, Pos.x + Size.x - ExcitementBarWidth, Pos.y + ExcitementBarTriggerLevelMarkingY, ExcitementBarWidth, 1);

		// frame
		SetColor(g, GetExcitementColor(Color.BLACK));
		DrawRect(g, Pos.x, Pos.y, Size.x, Size.y);

		if (IsInLearningNeuronState)
		{
			SetColor(g, GetExcitementColor(Color.GRAY));
			DrawRect(g, Pos.x - 1, Pos.y - 1, Size.x + 2, Size.y + 2);
			DrawRect(g, Pos.x - 2, Pos.y - 2, Size.x + 4, Size.y + 4);
			DrawRect(g, Pos.x - 3, Pos.y - 3, Size.x + 6, Size.y + 6);
		}

		// level markings
		SetColor(g, Color.BLACK);
		if (InputFieldWidth > 0)
		{
			DrawString(g, "|0", Pos.x - 4, Pos.y - 6);
			DrawString(g, "|1", Pos.x + InputFieldWidth - 4, Pos.y - 6);
		}
		DrawString(g, "-1", Pos.x + Size.x, Pos.y + 4);
		DrawString(g, "-0", Pos.x + Size.x, Pos.y + Size.y + 4);

		/*// not useful (no real information for user), needs CPU time
		int Stepy = Size.y / StrengthMax;
		for (int m = 0; m < StrengthMax; m++)
		{
			DrawLine(g, Pos.x + Size.x - 1 - 4, Pos.y + m * Stepy, Pos.x + Size.x - 1, Pos.y + m * Stepy);
		}
		*/

		// draw Hormone info
		if (ZoomManager.GetZoomFactor() >= 1.0) // we assume that otherwise DrawString() doesn't draw anything, too
		{
			for (int m = 0; m < HormoneNames.size(); m++)
			{
				String HormoneInfoString = HormoneNames.get(m) + "=" + ((int) (HormoneStrengths.get(m) * 100.0)) + "%";
				int HormoneInfoStringDrawWidth = 3 + HormoneInfoString.length() * 12; // we use the font "Courier New", which is a monospace font - all letters have the same width

				int DrawXPos = Pos.x + 4;
				int DrawYPos = Pos.y + Size.y - (HormoneNames.size() - 1) * 20 + m * 20 - 4;

				if (IsHighlighted || IsPartOfMultiSelection)
					SetColor(g, Color.GREEN);
				else
					SetColor(g, Color.LIGHT_GRAY);
				FillRect(g, DrawXPos - 3, DrawYPos - 16, HormoneInfoStringDrawWidth, 20);

				SetColor(g, Color.BLUE);
				DrawString(g, HormoneInfoString, DrawXPos, DrawYPos);
			}
		}
	}

	protected void ExciteAsPartner(double ExcitementAdd, DrawObject Sender)
	{
		double NormalizationFactor = 1.0; // we DO NOT use normalization any more, for two reasons: 1) unrealistic (does the real Neuron know how many synapses it has???), 2) too confusing, just summing up bar values is much easier to understand
		// discarded: 1.0 / (double) InputFields.size() / (double) ExcitementOnceCorrectionFactor;

		double HormoneFactor = 0.0;

		for (int m = 0; m < HormoneStrengths.size(); m++)
			HormoneFactor = Neuron.LimitHormoneStrength(HormoneFactor + HormoneStrengths.get(m));

		if (IsInLearningNeuronState && Sender.IsNegative)
		{
			// ignore excitement, negative NeuronInputField is just a "switch" to allow increasing any NeuronInputField.Strength value(s)
		}
		else
		{
			ExcitementCurrent += (ExcitementAdd * NormalizationFactor * (1.0 + HormoneFactor));
		}
	}

	@Override
	public ArrayList<DrawObject> GetChildDrawObjects()
	{
		ArrayList<DrawObject> ChildDrawObjects = new ArrayList<DrawObject>();

		for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
		{
			NeuronInputField o = i.next();
			ChildDrawObjects.add(o);
		}
		for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
		{
			NeuronInputNode o = i.next();
			ChildDrawObjects.add(o);
		}
		for (Iterator<NeuronOutputNode> i = OutputNodes.iterator(); i.hasNext();)
		{
			NeuronOutputNode o = i.next();
			ChildDrawObjects.add(o);
		}

		return ChildDrawObjects;
	}

	public double GetHormoneStrength(String HormoneName)
	{
		for (int m = 0; m < HormoneNames.size(); m++)
		{
			if (HormoneNames.get(m).equals(HormoneName))
			{
				return HormoneStrengths.get(m);
			}
		}
		return Double.MAX_VALUE;
	}

	@Override
	protected void LoseExcitementAndAddExcitementForNextTick()
	{
		// Neuron-specific: in EACH simulation tick, set it to zero, because NeuronInputFields do always in EACH tick re-donate their COMPLETE flow!

		ExcitementCurrent = LimitExcitement(ExcitementCurrent);

		ShiftExcitementHistory(ExcitementCurrent);

		// Neuron-specific:
		double ForwardedExcitement = LimitExcitement(ExcitementCurrent);

		if (DoesActivate(ForwardedExcitement))
		{
			((NeuronOutputNode) ExcitementPartner).ExciteAsPartner(ForwardedExcitement, this);
		}
		// end of specific

		// reset
		ExcitementCurrent = 0.0;
		ExcitementAddForNextTick = 0.0;
	}

	@Override
	public void Move(Point MoveAmount)
	{
		for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
		{
			NeuronInputField o = i.next();
			o.Move(MoveAmount);
		}
		for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
		{
			NeuronInputNode o = i.next();
			o.Move(MoveAmount);
		}
		for (Iterator<NeuronOutputNode> i = OutputNodes.iterator(); i.hasNext();)
		{
			NeuronOutputNode o = i.next();
			o.Move(MoveAmount);
		}

		Pos.x += MoveAmount.x;
		Pos.y += MoveAmount.y;
	}

	public void ReplaceHormoneName(String HormoneNameIn, String HormoneNameOut)
	{
		for (int i = 0; i < HormoneNames.size(); i++)
		{
			if (HormoneNames.get(i).equals(HormoneNameIn))
			{
				HormoneNames.set(i, HormoneNameOut);
			}
		}

		AutoSize();
	}

	@Override
	public void SetHighlighted(boolean IsHighlightedNew)
	{
		IsHighlighted = IsHighlightedNew;

		for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
		{
			NeuronInputField o = i.next();
			o.SetHighlighted(IsHighlighted);
		}
		for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
		{
			NeuronInputNode o = i.next();
			o.SetHighlighted(IsHighlighted);
		}
		for (Iterator<NeuronOutputNode> i = OutputNodes.iterator(); i.hasNext();)
		{
			NeuronOutputNode o = i.next();
			o.SetHighlighted(IsHighlighted);
		}
	}

	public void SetHormoneStrength(String ToUpdateName, double ToUpdateStrengthNew)
	{ // just does nothing if HormoneName not found, you can safely call this method even in this case

		for (int m = 0; m < HormoneNames.size(); m++)
		{
			if (HormoneNames.get(m).equals(ToUpdateName))
			{
				HormoneStrengths.set(m, LimitHormoneStrength(ToUpdateStrengthNew));
			}
		}
	}

	@Override
	public void SetPartOfMultiSelection(boolean IsPartNew)
	{
		IsPartOfMultiSelection = IsPartNew;

		for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
		{
			NeuronInputField o = i.next();
			o.SetPartOfMultiSelection(IsPartOfMultiSelection);
		}
		for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
		{
			NeuronInputNode o = i.next();
			o.SetPartOfMultiSelection(IsPartOfMultiSelection);
		}
		for (Iterator<NeuronOutputNode> i = OutputNodes.iterator(); i.hasNext();)
		{
			NeuronOutputNode o = i.next();
			o.SetPartOfMultiSelection(IsPartOfMultiSelection);
		}
	}

	@Override
	public void SetPos(Point PosNew)
	{
		PosNew = AlignOnGrid(PosNew);

		Point MoveAmount = new Point(PosNew.x - Pos.x, PosNew.y - Pos.y);

		Move(MoveAmount);
	}

	public void Toggle(Point MousePos)
	{
		for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
		{
			NeuronInputField o = i.next();
			if (o.ContainsPoint(MousePos))
				o.Toggle();
		}
	}

	public void ToggleInputFieldByIndex(int InputFieldIndex)
	{
		if (InputFieldIndex >= 0 && InputFieldIndex < InputFields.size())
		{
			if (InputFields.get(InputFieldIndex).Strength >= InputFields.get(InputFieldIndex).StrengthMax)
			{
				InputFields.get(InputFieldIndex).ToggleIsNegative();
			}
			InputFields.get(InputFieldIndex).Toggle();
		}
	}

	public void ToggleIsNegative(Point MousePos)
	{
		for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
		{
			NeuronInputField o = i.next();
			if (o.ContainsPoint(MousePos))
			{
				o.ToggleIsNegative();
			}
		}
	}
}
